/**
 * Code Sample for
 * Runtime Performance & Availability Monitoring for Java Systems.
 * IBM DevelperWorks.
 * Nicholas Whitehead (whitehead.nicholas@gmail.com)
 */

package org.digitarts.rtms.spring.collectors.agent.jms;

import java.io.File;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.jms.ConnectionFactory;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import javax.naming.Context;
import javax.naming.InitialContext;

import org.apache.log4j.Logger;
import org.digitarts.rtms.helpers.BeanHelper;
import org.digitarts.rtms.helpers.StringHelper;
import org.digitarts.rtms.helpers.XMLHelper;
import org.digitarts.rtms.spring.collectors.shell.IShell;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * <p>
 * Title: JMSAgent
 * </p>
 * <p>
 * Description: An agent implementation that listens on a JMS topic for a
 * collection request, collects and responds.
 * </p>
 * 
 * @author whitehead.nicholas@gmail.com
 * @version $Revision$
 */

public class JMSAgent implements MessageListener
{
	/** the jndi connection properties */
	protected Properties			jndiProperties			= new Properties();
	/** the jms connection factory name */
	protected String				connectionFactoryName	= null;
	/** the target jms topic name for subscribing to */
	protected String				topicName				= null;
	/** the target jms topic name for responding to */
	protected String				topicRespondName		= null;
	/** the shell adapater to issue local requests against */
	protected String				shellClassName			= null;
	/** the shell adapter configuration parameters */
	protected Map<String, String>	shellConfigMap			= new HashMap<String, String>();
	/** the shell to invoke commands against */
	protected IShell				shell					= null;
	/** An automatically assigned client name */
	protected String				autoClientName			= null;
	/** synchronization lock on publisher */
	protected Object				lock					= new Object();
	/** The command set, used for the selector */
	protected String				commandSet				= null;
	/** The short host name */
	protected String				shortHostName			= null;

	// =============================================================================================
	// JMS Fields
	// =============================================================================================
	protected ConnectionFactory		connectionFactory		= null;
	protected TopicConnection		topicConnection			= null;
	protected TopicSession			topicSessionSubscribe	= null;
	protected TopicSession			topicSessionPub			= null;
	protected TopicSession			topicSessionSub			= null;
	protected TopicSubscriber		topicSubscriber			= null;
	protected TopicPublisher		topicPublisher			= null;
	protected Topic					topicSub				= null;
	protected Topic					topicPub				= null;
	// =============================================================================================

	protected static Logger			log						= Logger.getLogger(JMSAgent.class);

	public JMSAgent()
	{
		log.info("JMSAgent Instantiated");
	}

	/**
	 * XML based constructor.
	 * 
	 * @param xml
	 */
	public JMSAgent(Element xml)
	{
		this();
		String propsStr = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(xml, "jndi", false)).trim();
		jndiProperties.putAll(StringHelper.toProperties(propsStr));
		connectionFactoryName = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(xml, "connectionFactory", false));
		topicRespondName = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(xml, "respondTopic", false));
		topicName = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(xml, "listenTopic", false));
		shellClassName = XMLHelper.getAttributeValueByName(XMLHelper.getChildNodeByName(xml, "shellClass", false), "name");
		commandSet = XMLHelper.getNodeTextValue(XMLHelper.getChildNodeByName(xml, "commandSet", false)).trim();
		List<Node> propertyNodes = XMLHelper.getChildNodesByName(XMLHelper.getChildNodeByName(xml, "shellClass", false), "property", false);
		for (Node pNode : propertyNodes)
		{
			shellConfigMap.put(XMLHelper.getAttributeValueByName(pNode, "name"), XMLHelper.getAttributeValueByName(pNode, "value"));
		}
		configShell();
		deriveClientName();
	}

	protected void deriveClientName()
	{
		StringBuilder buff = new StringBuilder("JMSAgent.");
		StringBuilder buff2 = new StringBuilder(JMSAgentConstants.TRACING_NAMESPACE_DELIM);
		buff.append(System.getProperty("os.name")).append(".");
		buff2.append(System.getProperty("os.name")).append(JMSAgentConstants.TRACING_NAMESPACE_DELIM);
		try
		{
			buff.append(java.net.InetAddress.getLocalHost().getCanonicalHostName());
			buff2.append(java.net.InetAddress.getLocalHost().getHostName());
		}
		catch (UnknownHostException e)
		{
			buff.append("UnknownName");
			buff2.append("UnknownName");
		}
		autoClientName = buff.toString().replaceAll("\\s+", "");
		shortHostName = buff2.toString().replaceAll("\\s+", "");
	}

	protected void configShell()
	{
		try
		{
			Class<?> clazz = Class.forName(shellClassName);
			shell = (IShell) clazz.newInstance();
			for (Entry<String, String> entry : shellConfigMap.entrySet())
			{
				BeanHelper.setAttribute(entry.getKey(), entry.getValue(), shell);
			}
		}
		catch (Exception e)
		{
			log.error("Failed to config shell", e);
			throw new RuntimeException("Failed to config shell", e);
		}
	}

	/**
	 * Initializes the JMS subscription.
	 * 
	 * @throws Exception
	 */
	public void initializeJMS() throws Exception
	{
		// create jndi context, lookup connectionFactory, lookup topic, prepare
		// publisher, start subscription
		Context ctx = null;
		try
		{
			ctx = new InitialContext(jndiProperties);
			connectionFactory = (ConnectionFactory) ctx.lookup(connectionFactoryName);
			topicSub = (Topic) ctx.lookup(topicName);
			topicPub = (Topic) ctx.lookup(topicRespondName);
			topicConnection = (TopicConnection) connectionFactory.createConnection();

			topicConnection.setClientID(autoClientName);
			topicSessionSub = topicConnection.createTopicSession(false, TopicSession.AUTO_ACKNOWLEDGE);
			topicSessionPub = topicConnection.createTopicSession(false, TopicSession.AUTO_ACKNOWLEDGE);
			topicSubscriber = topicSessionSub.createSubscriber(topicSub, JMSAgentConstants.COMMAND_SET_BEAN_NAME + " = '" + commandSet + "'", false);
			topicPublisher = topicSessionPub.createPublisher(topicPub);
			topicSubscriber.setMessageListener(this);
			topicConnection.start();
			log.info("Started Subscription to " + ctx.getEnvironment().get(Context.PROVIDER_URL) + "/" + topicSub.getTopicName() + "\n\tFrom:" + autoClientName);
		}
		catch (Exception e)
		{
			log.error("Failed to init JMS", e);
			closeJMS();
			throw e;
		}
		finally
		{
			try
			{
				ctx.close();
			}
			catch (Exception e)
			{
			}
		}
	}

	/**
	 * Closes all JMS resources.
	 */
	public void closeJMS()
	{
		log.info("Closing JMS");
		try
		{
			topicSubscriber.close();
		}
		catch (Exception e)
		{
		}
		;
		try
		{
			topicPublisher.close();
		}
		catch (Exception e)
		{
		}
		;
		try
		{
			topicSessionSub.close();
		}
		catch (Exception e)
		{
		}
		;
		try
		{
			topicSessionPub.close();
		}
		catch (Exception e)
		{
		}
		;
		try
		{
			topicConnection.close();
		}
		catch (Exception e)
		{
		}
		;
		try
		{
			topicConnection.stop();
		}
		catch (Exception e)
		{
		}
		;
		log.info("Closed JMS");
	}

	/**
	 * Entry point for requests.
	 * 
	 * @param message
	 */
	public void onMessage(Message message)
	{
		// if(log.isDebugEnabled())log.debug("Message Received:\n" + message +
		// "\n====================");
		try
		{
			String command = ((TextMessage) message).getText();
			if (log.isDebugEnabled())
				log.debug("Command Request:" + command);
			String result = shell.issueOSCommand(command);
			synchronized (lock)
			{
				TextMessage response = topicSessionPub.createTextMessage(result);
				response.setStringProperty(JMSAgentConstants.COMMAND_SET_BEAN_NAME, message.getStringProperty(JMSAgentConstants.COMMAND_SET_BEAN_NAME));
				response.setStringProperty(JMSAgentConstants.TRACING_NAMESPACE, message.getStringProperty(JMSAgentConstants.TRACING_NAMESPACE) + shortHostName);
				response.setStringProperty(JMSAgentConstants.COMMAND_SET_COMMAND_NAME, command);
				topicPublisher.send(response);
			}
			if (log.isDebugEnabled())
				log.debug("Result:\n" + result);
		}
		catch (Exception e)
		{
			log.error("Failed to process command request", e);
		}

	}

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		log.info("JMSAgent Config:" + args[0]);
		Element xml = XMLHelper.parseXML(new File(args[0])).getDocumentElement();
		JMSAgent agent = new JMSAgent(xml);
		try
		{
			agent.initializeJMS();
			// Thread.currentThread().join();
		}
		catch (Exception e)
		{
			log.error("JMSAgent could not be started", e);
		}
	}

	/**
	 * @return the jndiProperties
	 */
	public Properties getJndiProperties()
	{
		return jndiProperties;
	}

	/**
	 * @param jndiProperties
	 *            the jndiProperties to set
	 */
	public void setJndiProperties(Properties jndiProperties)
	{
		this.jndiProperties = jndiProperties;
	}

	/**
	 * @return the connectionFactoryName
	 */
	public String getConnectionFactoryName()
	{
		return connectionFactoryName;
	}

	/**
	 * @param connectionFactoryName
	 *            the connectionFactoryName to set
	 */
	public void setConnectionFactoryName(String connectionFactoryName)
	{
		this.connectionFactoryName = connectionFactoryName;
	}

	/**
	 * @return the topicName
	 */
	public String getTopicName()
	{
		return topicName;
	}

	/**
	 * @param topicName
	 *            the topicName to set
	 */
	public void setTopicName(String topicName)
	{
		this.topicName = topicName;
	}

	/**
	 * @return the shellClassName
	 */
	public String getShellClassName()
	{
		return shellClassName;
	}

	/**
	 * @param shellClassName
	 *            the shellClassName to set
	 */
	public void setShellClassName(String shellClassName)
	{
		this.shellClassName = shellClassName;
	}

	/**
	 * @return the shellConfigMap
	 */
	public Map<String, String> getShellConfigMap()
	{
		return shellConfigMap;
	}

	/**
	 * @param shellConfigMap
	 *            the shellConfigMap to set
	 */
	public void setShellConfigMap(Map<String, String> shellConfigMap)
	{
		this.shellConfigMap = shellConfigMap;
	}

	/**
	 * @return the autoClientName
	 */
	public String getAutoClientName()
	{
		return autoClientName;
	}

}
